Содержание статьи
Версия Chrome, о которой пойдет речь, — 87.0.4280.141. А интересующая нас запатченная уязвимость — CVE-2021-21112. Она касается компонента потоков компрессии в браузерном движке Blink и работает по принципу use after free. О баге сообщил исследователь YoungJoo Lee (@ashuu_lee) из компании RaonWhiteHat в ноябре 2020 года через сайт bugs.chromium.org, номер отчета 1151298.
Blink — это браузерный движок, на основе которого работает Chrome. А потоки сжатия — это те же веб‑потоки (web streams), но для удобства веб‑разработчиков передающиеся со сжатием. Чтобы не приходилось тянуть за проектом зависимости типа zlib, создатели Chrome решили интегрировать форматы сжатия gzip и deflate в движок Blink.
По сути, это удобная обертка, трансформирующий поток с алгоритмом трансформации данных по умолчанию (или gzip, или deflate). Трансформирующий поток — это объект, содержащий два потока: читаемый (readable) и записываемый (writable). А между ними находится трансформер, который применяет заданный алгоритм к проходящим между ними данным.
В статье я буду ссылаться на старые версии спецификации потоков и исходного кода. По понятным причинам исходный код с тех пор изменился. Да и спецификация тоже.
Стенд
Для воспроизведения уязвимости понадобится стенд, состоящий из виртуальной машины и уязвимой версии Chrome. Готовую виртуальную машину можно загрузить с сайта osboxes.org. Сайт предоставляет образы виртуальных машин как для VirtualBox, так и для VMware.
Я буду использовать образ Xubuntu 20 для VirtualBox. Читатель волен выбирать любой дистрибутив. Запускаем машину, обновляемся:
sudo apt update && sudo apt upgrade -y
Теперь нам нужна уязвимая версия браузера.
Уязвимую версию Chrome, скомпилированную с ASan (AddressSanitizer), можно скачать с googleapis.com. В отчете об уязвимости указано название нужной сборки, а именно билд asan-linux-release-812852. Распаковываем архив:
unzip asan-linux-release-812852.zip
Готовый билд сэкономит кучу времени, так как сборка браузера требует времени, особенно если машина не очень мощная.
AddressSanitizer — это детектор ошибок памяти. Он предоставляет инструментацию во время компиляции кода и библиотеку времени выполнения (runtime). Подробнее о нем можно почитать на сайте Clang.
Теперь у нас готова виртуальная машина и скачан необходимый билд Chrome. Помимо них, нам понадобится Python 3 и LLVM. Обычно лог санитайзера ASan выглядит нечитаемо, поскольку там указаны только адреса и смещения. Разобраться поможет утилита llvm-symbolizer, которая устанавливается вместе с LLVM. Она читает эти адреса и смещения и выводит соответствующие места в исходном коде. Лог ASan будет выглядеть намного понятнее.
Ну а Python поможет нам готовить данные для сжатия.
Все установлено, теперь в бой!
Теория
Прежде чем разбираться в деталях уязвимости, нам нужно немного понимать предметную область.
Предыстория всего этого такова. В конце 2019 года команда разработчиков Chromium реализовала новый JavaScript API, который называется Compression Streams. Детали реализации приведены в отчете.
Этот API основан на спецификации потоков (спецификация от 30 января 2020 года). Подробно с его концепцией можешь ознакомиться в дизайн‑документе, дополнительные пояснения смотри на GitHub.
Я привожу более старые версии, так как уязвимость касалась именно их реализации. В дальнейшем спецификация потоков и их реализация в Chromium изменилась.
Теперь разберемся в потоках преобразования, потоках сжатия, объектах promise и методе postMessage
.
Потоки сжатия
Потоки сжатия основаны на концепции и реализации веб‑потоков. Отличие в том, что потоки компрессии могут сжимать и распаковывать данные. На выбор — алгоритмы gzip и deflate, широко применяемые в веб‑технологиях. Потоки компрессии удовлетворяют спецификации transform stream.
Ниже приведена схема алгоритма.
Грубо говоря, если данные не кончились (считан чанк), то вызывается метод Transform
, а тот вызовет метод компрессии или декомпрессии — в данном случае Inflate. В этом методе данные обрабатываются в цикле. Затем они помещаются в очередь потока. Для этого вызывается метод Enqueue
.
То есть обрабатываем куски данных и кладем их в очередь.
Promise
JavaScript часто описывают как язык прототипного наследования. Каждый объект имеет объект‑прототип — шаблон методов и свойств. Все объекты имеют общий прототип Object.
и свой отдельный.
Поэтому при изменении каких‑то свойств или методов прототипа новые объекты будут обладать измененными свойствами или методами.
Далее нас интересуют асинхронное программирование и «обещания» (promise). Раньше JavaScript исполнялся синхронно, но это мешало веб‑страницам быстро загружаться и плавно работать. Асинхронное программирование позволяет обойти эту проблему. При ожидании какой‑то операции (загрузки данных по сети, чтения с диска и тому подобных) основной поток приложения не блокируется, и оно не подвисает.
Сначала в JavaScript внедрили асинхронные колбэки (вызовы функций по завершении операции). Позднее придумали новый стиль написания асинхронного кода — «обещания». Promise — это объект, представляющий асинхронную операцию, выполненную удачно или неудачно. На картинке это наглядно изображено.
Промис — это как бы промежуточное состояние: «Я обещаю вернуться к вам с результатом как можно скорее».
У объекта promise есть метод then
. Он принимает два параметра: функции, которые нужно вызвать в случае разрешения (resolve) или в случае отклонения (reject). В зависимости от результата будет вызвана соответствующая.
Особенность JavaScript в том, что в этом языке все является объектом. По сути метод или функция — это тоже объект. И доступ к нему — это вызов объектов get
и set
объекта — прототипа объекта. Красиво?
Особенность объекта promise в том, что при его разрешении (resolve) необходимо вызвать then
. Доступ к этому методу (get
) можно изменить на пользовательский код, сменив общий для всех объектов прототип:
Object.defineProperty(Object.prototype, "then", { get() { console.log("then getter executed"); }});
postMessage
Как мы можем узнать из MDN Web Docs, этот метод позволяет обмениваться данными между объектами типа Window
, например между страницей и фреймом. Интересная особенность заключается в том, как передаются данные.
postMessage(message, targetOrigin, transfer);
После вызова функции владение transfer
передается адресату, а на передающей стороне прекращается.
Если вкратце, суть уязвимости в том, что обработка большого массива данных происходит в цикле и при добавлении обработанных чанков в очередь есть возможность вызвать пользовательский код на JS. Это обеспечено тем, что объекту promise дано разрешение на чтение из потока. Пользовательский код через postMessage
может освободить данные, которые на тот момент обрабатывались в цикле.
Для более детального понимания всех концепций можно обратиться к спецификации. Мы же переходим к практике.
Запуск PoC
Первым делом нужно установить LLVM, так как с ним поставляется симболизатор. Без него стек вызовов будет выглядеть непонятно, поскольку не будет названий методов и имен файлов.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»