Эксплойты нулевого дня в операции WizardOpium

Еще в октябре 2019 года мы обнаружили классическую атаку типа watering hole на сайт, публикующий новости Северной Кореи. Злоумышленники использовали цепочку уязвимостей нулевого дня в Google Chrome и Microsoft Windows. В своем блоге мы уже публиковали посты с кратким описанием этой операции (с ними можно ознакомиться здесь и здесь), однако сейчас мы хотели бы углубиться в технические детали эксплойтов и уязвимостей, использованных в этой атаке.

Эксплойт для удаленного выполнения кода в Google Chrome

В первом посте мы описали загрузчик эксплойта, отвечающий за первичную проверку версии браузера и выполнение JavaScript-кода, содержащего полный эксплойт. Он достаточно объемный, так как помимо кода содержит байтовые массивы с шелл-кодом, файл Portable Executable (PE) и модуль WebAssembly (WASM), используемый на следующих стадиях атаки. Зловред использовал уязвимость в интерфейсе WebAudio OfflineAudioContext и предназначался для Google Chrome 76.0.3809.87 и 77.0.3865.75. Однако эта уязвимость существует уже достаточно давно, и гораздо более ранние релизы с компонентом WebAudio тоже ей подвержены. На момент обнаружения эксплойта наиболее актуальной была версия Google Chrome 78. И хотя она также была затронута, эксплойт не поддерживал ее и выполнял ряд проверок, чтобы убедиться, что имеет дело только с целевыми сборками. В противном случае атака могла вызвать сбой в работе браузера. Мы сообщили Google об этой уязвимости, после чего ей был присвоен идентификатор CVE-2019-13720, а сама она была устранена в версии 78.0.3904.87 со следующим коммитом. Use-after-free (UAF) уязвимость, которая может быть вызвана состоянием гонки между потоками Render и Audio:

Как видно из кода, когда аудиобуфер в ConvolverNode сбрасывается, а активный буфер уже существует в объекте Reverb, функция SetBuffer() может уничтожить объекты reverb_ и shared_buffer_.

Однако эти объекты могут и дальше использоваться потоком Render, так как в коде не реализована надлежащая синхронизация между двумя потоками. Исправление добавило две недостающих блокировки (graph lock и process lock) на момент сбрасывания буфера.

Код эксплойта был обфусцирован, но мы смогли полностью восстановить его посредством реверс-инжиниринга. Пристальное изучение кода показало, что автор эксплойта хорошо знаком с внутренним устройством отдельных компонентов Google Chrome, в частности с аллокатором памяти PartitionAlloc. Об этом свидетельствуют представленные ниже фрагменты восстановленного кода. Эти функции используются для получения информации из внутренних структур аллокатора, такой как: адрес SuperPage, адрес PartitionPage по индексу внутри SuperPage, индекс используемой PartitionPage и адрес метаданных PartitionPage. Все константы заимствованы из partition_alloc_constants.h:

Интересно, что для работы с 64-битными значениями эксплойт также использует относительно новый встроенный класс BigInt (авторы обычно используют в эксплойтах собственные примитивы).

Сначала код инициализирует OfflineAudioContext и создает большое количество объектов IIRFilterNode, которые инициализируются через два массива данных типа float.

После этого эксплойт приступает к начальной стадии атаки и пытается спровоцировать ошибку UAF. Для этого он создает объекты, необходимые для компонента Reverb, включая еще один огромный объект OfflineAudioContext и два объекта ConvolverNode: ScriptProcessorNode для начала обработки аудио и AudioBuffer для аудиоканала.

Эта функция выполняется рекурсивно. Она заполняет буфер аудиоканала нулями, начинает рендеринг в режиме офлайн и в то же время запускает цикл, который обнуляет и сбрасывает буфер канала объекта ConvolverNode, тем самым пытаясь спровоцировать ошибку. Эксплойт использует функцию later() для имитации функции Sleep, приостановки текущего потока и позволяет потокам Render и Audio завершить выполнение вовремя:

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

Аллокатор памяти PartitionAlloc располагает специальными средствами защиты от эксплойтов: при освобождении области памяти выполняется перестановка байтов в адресе указателя, после чего такой адрес добавляется в структуру FreeList. Это усложняет эксплуатацию уязвимости, так как попытка разыменования подобного указателя приведет к сбою процесса. Для обхода этой техники эксплойт использует следующий примитив, который просто выполняет обратную перестановку байтов в адресе указателя:

Эксплойт использует «утекший» указатель для получения адреса структуры SuperPage и проверяет его. Если все идет по плану, то это должен быть «сырой» указатель на объект temporary_buffer_ класса ReverbConvolverStage, который передается в функцию обратного вызова initialUAFCallback.

Эксплойт использует «утекший» указатель для получения адреса «сырого» указателя на массив feedforward_ с типом данных AudioArray<double>, который присутствует в объекте IIRProcessor, созданном с помощью IIRFilterNode. Этот массив должен находиться в той же структуре SuperPage, однако в разных версиях Chrome этот объект попадает в разные PartitionPage, поэтому внутри initialUAFCallback находится специальный код для высчитывания корректного адреса.

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

В этот раз эксплойт использует функцию getFrequencyResponse() для проверки успешности эксплуатации уязвимости. Эта функция создает массив частот, который заполняется с помощью фильтра Найквиста, а исходный массив для данной операции заполняется нулями.

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

Дефрагментация кучи выполняется посредством нескольких вызовов импровизированной функции collectGarbage, которая создает в цикле огромный ArrayBuffer.

После этих шагов эксплойт выполняет функцию kickPayload(), передавая ранее созданный массив BigUint64Array, содержащий адрес «сырого» указателя ранее освобожденных данных AudioArray.

Эксплойт манипулирует метаданными PartitionPage освобожденного объекта, чтобы вызвать следующее поведение: если адрес другого объекта записывается в массив BigUint64Array с нулевым индексом и при этом создается новый 8-байтовый объект, а затем считывается значение, расположенное по индексу 0, то в этом случае будет считано значение, расположенное по ранее установленному адресу. Если на этом этапе что-то записывается с индексом 0, то это значение будет записано по ранее установленному адресу.

После создания примитивов произвольного чтения и записи памяти наступает финальная стадия — выполнение кода. Эксплойт достигает этого с помощью популярной техники с использованием функциональности Web Assembly (WASM). В настоящее время Google Chrome выделяет страницы для кода, полученного методом JIT-компиляции, с правами на чтение, запись и исполнение (RWX), что позволяет перезаписать их шелл-кодом. Сначала эксплойт инициирует фиктивный модуль WASM, в результате чего выделяются страницы памяти для JIT-кода.

Для выполнения экспортированной функции wasmFuncA эксплойт создает объект FileReader. Когда он инициализируется данными, он создает внутри объект FileReaderLoader. Если вы можете работать со структурой аллокатора PartitionAlloc и знаете размер следующего размещаемого в памяти объекта, то вы можете предсказать, по какому адресу он будет размещен. Эксплойт использует функцию getPartitionPageFreeListHeadEntryBySlotSize() с указанным размером и получает адрес следующего свободного блока, который будет занят FileReaderLoader.

Эксплойт получает этот адрес дважды, чтобы узнать, был ли создан объект FileReaderLoader и может ли эксплойт продолжить выполнение. Эксплойт превращает экспортированную функцию WASM в функцию обратного вызова для события FileReader (конкретно в этом случае для события onerror), а поскольку тип FileReader является производным от EventTargetWithInlineData, его можно использовать для получения адресов всех его событий и адреса скомпилированной на лету экспортированной функции WASM.

Переменная stubAddr содержит адрес страницы с кодом заглушки, который переходит на функцию WASM, полученную методом JIT-компиляции. На данном этапе достаточно перезаписать его шелл-кодом. Для этого эксплойт снова использует функцию getPartitionPageFreeListHeadEntryBySlotSize(), чтобы найти следующий свободный блок размером 0x20 байт, что соответствует размеру структуры для объекта ArrayBuffer. Этот объект создается, когда эксплойт создает новый аудиобуфер.

Эксплойт использует примитивы произвольного чтения и записи для получения адреса класса DataHolder, который содержит «сырой» указатель на данные и размер аудиобуфера. Эксплойт перезаписывает этот указатель с помощью stubAddr и задает для него огромный размер.

Теперь достаточно имплантировать объект Uint8Array в память этого аудиобуфера и поместить туда шелл-код вместе с файлом Portable Executable, который будет выполняться шелл-кодом.

Чтобы предотвратить возможный сбой, эксплойт очищает указатель до вершины структуры FreeList, используемой PartitionPage.

Теперь для выполнения шелл-кода достаточно вызвать экспортированную функцию WASM.

Эксплойт для повышения привилегий в Microsoft Windows

Шелл-код оказался рефлексивным PE-загрузчиком для модуля, который был также обнаружен в составе эксплойта. Этот модуль в основном состоял из кода для выхода за пределы песочницы Google Chrome, использующего уязвимость компонента ядра Windows win32k для повышения привилегий, а также отвечал за загрузку и выполнение вредоносной программы. При более тщательном анализе выяснилось, что эксплуатируемая ошибка по факту оказалась уязвимостью нулевого дня. Мы уведомили Microsoft Security Response Center, после чего они присвоили ей идентификатор CVE-2019-1458 и затем устранили. У компонента win32k сложилась дурная репутация. Он присутствует в системе еще с Windows NT 4.0 и, по данным Microsoft, в более чем половине случаев оказывается причиной ошибок, связанных с безопасностью ядра. Только за последние два года «Лаборатория Касперского» обнаружила на просторах Сети пять угроз нулевого дня, которые использовали уязвимости win32k. Это довольно интересная статистика, учитывая, что с момента выхода Windows 10 компания Microsoft внесла ряд изменений, усложняющих эксплуатацию уязвимостей win32k, и большинство найденных нами угро нулевого дня эксплуатировали версии Microsoft Windows до выхода Windows 10 RS4. Эксплойт для повышения привилегий, задействованный в операции WizardOpium, предназначался для использования в Windows 7, Windows 10 build 10240 и Windows 10 build14393. Также важно отметить, что в Google Chrome встроена специальная функция безопасности Win32k lockdown, которая предупреждает любые атаки на win32k, отключая доступ к системным вызовам win32k из процессов Chrome. К сожалению, Win32k lockdown поддерживается только на устройствах под управлением Windows 10. Поэтому логично предположить, что операция WizardOpium была нацелена на пользователей Windows 7.

Уязвимость CVE-2019-1458 связана с разыменованием произвольных указателей. В win32k объекты Window представлены структурой tagWND. Также существует ряд классов, основанных на этой структуре: ScrollBar, Menu, Listbox, Switch и прочие. Поле FNID структуры tagWND используется для определения типа класса. Разные классы также имеют различные дополнительные данные, прилагаемые к структуре tagWND. Эти дополнительные данные по большому счету представляют собой разнообразные структуры, часто содержащие указатели ядра. Кроме того, в компоненте win32k доступен системный вызов SetWindowLongPtr, с помощью которого можно задавать эти дополнительные данные (разумеется, после проверки). Стоит отметить, что SetWindowLongPtr в прошлом уже упоминался в контексте других уязвимостей (например, CVE-2010-2744, CVE-2016-7255 и CVE-2019-0859). Существует распространенная проблема, когда предварительно инициализированные дополнительные данные могут привести к некорректной обработке системных процедур. В случае с CVE-2019-1458 проверка, выполняемая в функции SetWindowLongPtr, была просто неэффективна.

Проверка параметра индекса предотвратила бы эту ошибку, однако до выхода исправления значения для FNID_DESKTOP, FNID_SWITCH, FNID_TOOLTIPS внутри таблицы mpFnid_serverCBWndProc не были инициализированы, что делало эту проверку бесполезной и позволяло перезаписать указатели ядра внутри дополнительных данных.

Вызвать эту ошибку довольно просто: сначала нужно создать объект Window, после чего NtUserMessageCall можно использовать для вызова любой процедуры системного класса Window.

Здесь важно указать правильные параметры сообщения и dwType. Сообщение должно быть равно WM_CREATE. dwType изнутри преобразуется в fnIndex следующим образом: (dwType + 6) & 0x1F. Эксплойт использует dwType, равный 0xE0. В результате получается fnIndex, равный 6, который является индексом функции xxxSwitchWndProc, а сообщение WM_CREATE делает поле FNID равным FNID_SWITCH.

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

На текущем этапе достаточно снова вызвать NtUserMessageCall, но на этот раз с сообщением, равным WM_ERASEBKGND. Это приведет к выполнению функции xxxPaintSwitchWindow, которая инкрементирует и декрементирует пару целых чисел, расположенных по указателю, установленному ранее.

Важным условием для активации пути к эксплуатируемому коду является нажатие клавиши ALT.

Уязвимость эксплуатируется за счет использования объектов типа Bitmap. Для успешной атаки несколько объектов Bitmap должны быть размещены в памяти рядом друг с другом, а их адреса внутри ядра должны быть известны. Для этого эксплойт использует две распространенные техники обхода ASLR ядра. В случае Windows 7 и Windows 10 build 10240 (Threshold 1) утечка адресов объектов Bitmap в ядре реализуется с помощью техники GdiSharedHandleTable: в старых версиях ОС на пользовательском уровне доступна специальная таблица, содержащая адреса всех объектов GDI, участвующих в процессе. Эта возможность была устранена в сборке Windows 10 14393 (Redstone 1), поэтому для этой версии эксплойт задействует другую распространенную технику, использующую таблицы акселераторов (исправлена в Redstone 2). Следуя этой технике, нужно создать объект Create Accelerator Table, вычленить его адрес в ядре из массива gSharedInfo HandleTable, доступного на пользовательском уровне, а затем освободить память, выделенную под объект Accelerator Table, и разместить объект Bitmap по тому же адресу в памяти.

Весь процесс эксплуатации уязвимости заключается в следующем: эксплойт создает три расположенных рядом объекта Bitmap, провоцируя утечку их адресов. Затем он подготавливает Switch Window и использует уязвимость в NtUserSetWindowLongPtr, чтобы выдать адрес, указывающий на конец первого объекта Bitmap, за дополнительный массив данных Switch Window. Объекты Bitmap представлены структурой SURFOBJ, и ранее установленный адрес необходимо вычислить таким образом, чтобы функция xxxPaintSwitchWindow инкрементировала поле sizlBitmap структуры SURFOBJ для объекта Bitmap, размещенного рядом с первым объектом. Поле sizlBitmap определяет границы буфера пиксельных данных, а инкрементированное значение позволяет использовать функцию SetBitmapBits() для неограниченной записи и перезаписи структуры SURFOBJ третьего объекта Bitmap.

Поле pvScan0 структуры SURFOBJ является адресом буфера пиксельных данных, поэтому, перезаписав его произвольным указателем, мы получаем примитивы произвольного чтения и записи с помощью функций GetBitmapBits()/SetBitmapBits(). Эксплойт использует эти примитивы для парсинга структуры EPROCESS и кражи токена системы. Для получения адреса в ядре для структуры EPROCESS эксплойт использует функцию EnumDeviceDrivers. Эта функция работает в соответствии с описанием в MSDN и предоставляет список адресов в ядре для загруженных в данный момент драйверов. Первый адрес в списке — адрес ntkrnl, и для получения адреса структуры EPROCESS эксплойт разбирает исполняемый файл, пытаясь найти экспортированную переменную PsInitialSystemProcess.

Стоит отметить, что эта техника до сих пор работает в последних версиях Windows (по результатам тестирования в Windows 10 19H1 build 18362). Кража токена системы — наиболее часто используемая техника эксплуатации уязвимости для повышения привилегий. После получения системных привилегий эксплойт загружает и исполняет саму вредоносную программу.

Выводы

Нам было чрезвычайно интересно изучить эту операцию, потому что это первый эксплойт нулевого дня для Google Chrome, обнаруженный «в диких условиях» за последнее время. Любопытно, что он использовался в сочетании с эксплойтом повышения привилегий, который блокируется в последних версиях Windows функцией безопасности Google Chrome Win32k lockdown. Что касается повышения привилегий, было не менее любопытно обнаружить еще один эксплойт для этой уязвимости всего через неделю после выхода исправления. Это наглядно демонстрирует, насколько легко ее использовать.

Мы хотели бы поблагодарить специалистов по безопасности Google Chrome и Microsoft за оперативное устранение этих уязвимостей. Компания Google предложила щедрое вознаграждение за выявление CVE-2019-13720. Вознаграждение было передано на благотворительность, после чего компания Google сделала аналогичное пожертвование.

Публикации на схожие темы

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

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